feat: add live screen recording preview#319
feat: add live screen recording preview#319hamidlabs wants to merge 9 commits intosiddharthvaddem:mainfrom
Conversation
📝 WalkthroughWalkthroughAdds Linux session-type detection and preload IPC; enables PipeWire capture flags on Linux; introduces a canvas-based live preview + preview lifecycle hook; extends recorder to accept preview handoff; refactors LaunchWindow to integrate preview and updates HUD overlay sizing/positioning. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c69debc1fe
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
electron/ipc/handlers.ts (1)
221-224: Consider normalizing session type to known values.The handler returns
process.env.XDG_SESSION_TYPEdirectly, which could contain unexpected values beyond "wayland" or "x11" (e.g., "tty", "mir", or custom values). If the consumer code expects only "wayland" or "x11", consider normalizing:♻️ Optional: Normalize to known session types
ipcMain.handle("get-session-type", () => { if (process.platform !== "linux") return "x11"; - return process.env.XDG_SESSION_TYPE || (process.env.WAYLAND_DISPLAY ? "wayland" : "x11"); + const sessionType = process.env.XDG_SESSION_TYPE?.toLowerCase(); + if (sessionType === "wayland") return "wayland"; + if (sessionType === "x11") return "x11"; + // Fallback detection + return process.env.WAYLAND_DISPLAY ? "wayland" : "x11"; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@electron/ipc/handlers.ts` around lines 221 - 224, The get-session-type IPC handler returns process.env.XDG_SESSION_TYPE directly which may be unexpected values; update the ipcMain.handle("get-session-type", ...) implementation to normalize the session type to only "wayland" or "x11": read XDG_SESSION_TYPE (and fall back to checking WAYLAND_DISPLAY as before), lowercase it, then return "wayland" if it equals "wayland" and return "x11" for any other value. Ensure you update the logic inside the ipcMain.handle callback so consumers always receive one of the two known values.src/components/launch/LaunchWindow.tsx (1)
145-172:prevSourceIdresets when effect dependencies change, potentially causing redundant preview starts.The
prevSourceIdvariable is declared inside the effect, so it resets tonullwhenever the effect re-runs due to dependency changes (recordingorstartPreview). This means when recording stops, the effect restarts,prevSourceIdbecomesnull, andstartPreviewmay be called even for the same source.♻️ Use useRef to persist prevSourceId across effect runs
+const prevSourceIdRef = useRef<string | null>(null); // Poll for source selection and start preview when source is picked useEffect(() => { - let prevSourceId: string | null = null; const checkSelectedSource = async () => { if (window.electronAPI) { const source = await window.electronAPI.getSelectedSource(); if (source) { setSelectedSource(source.name); setHasSelectedSource(true); // Auto-start preview when source changes - if (source.id !== prevSourceId && !recording) { - prevSourceId = source.id; + if (source.id !== prevSourceIdRef.current && !recording) { + prevSourceIdRef.current = source.id; startPreview(source.id); } } else { setSelectedSource("Screen"); setHasSelectedSource(false); } } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/launch/LaunchWindow.tsx` around lines 145 - 172, prevSourceId is declared inside the useEffect so it resets whenever the effect re-runs causing duplicate startPreview calls; move that state to a ref (e.g., prevSourceIdRef = useRef<string | null>(null)) at component scope and replace uses of prevSourceId in the effect with prevSourceIdRef.current, updating prevSourceIdRef.current = source.id after calling startPreview(source.id) and when selecting a source so the previous source id persists across effect runs; keep the rest of the checkSelectedSource logic and the interval handling the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 180-213: The setTimeout inside handleToggleRecording can fire
after unmount; add a ref (e.g., restartPreviewTimeoutRef) to store the timeout
id when scheduling the 500ms restart and use that ref to clearTimeout if needed,
update the callback to assign the timeout to restartPreviewTimeoutRef.current
instead of an anonymous timeout, and add a cleanup useEffect that clears
restartPreviewTimeoutRef.current on unmount to prevent calling startPreview
after the component has been unmounted.
In `@src/components/launch/LivePreview.tsx`:
- Around line 151-163: The effect in LivePreview that creates a video element
when streams.webcam becomes available can leak resources; update the useEffect
(the one that checks webcamVideoRef.current and streams?.webcam) to return a
cleanup function that tears down the created element: if webcamVideoRef.current
exists and its srcObject equals the stream, pause the video, remove it from the
DOM, set webcamVideoRef.current.srcObject = null, stop any tracks on the
MediaStream, and clear webcamVideoRef.current; this ensures toggling the webcam
or unmounting releases streams and DOM nodes even when the parent streams object
identity does not change.
- Around line 105-121: The crop math uses webcamVideo.videoWidth/height and can
divide by zero before metadata loads; inside the drawing routine in
LivePreview.tsx where ww, wh, aspectRatio, sx, sy, sw, sh are computed, guard by
checking webcamVideo.videoWidth and webcamVideo.videoHeight > 0 and bail out or
skip cropping/drawing until they are valid (or set safe defaults like 1) so
aspectRatio is never Infinity/NaN and drawImage gets valid sx/sy/sw/sh values.
In `@src/hooks/usePreviewStream.ts`:
- Around line 24-35: The preview startup/stop code must serialize and abort
stale webcam requests to avoid double prompts and leaked tracks: add a
latestRequestRef (useRef<symbol|null>) and, in startPreview and in the effect
that also calls getUserMedia, create a new unique symbol and assign it to
latestRequestRef before awaiting navigator.mediaDevices.getUserMedia; after the
await verify the symbol still matches latestRequestRef.current and only then
assign webcamStreamRef.current and setStreams; if it does not match, stop the
newly obtained tracks immediately; update stopPreview to clear
latestRequestRef.current (set to null) so late resolves know the request was
cancelled and always stop any stream returned by stale requests.
In `@src/hooks/useScreenRecorder.ts`:
- Around line 453-456: When reusing the preview handoff stream in
useScreenRecorder, the preview track must be upgraded to recording constraints
instead of using the preview resolution as-is; locate the branch that sets
webcamStream.current = previewHandoff.webcamStream and before assigning or
before starting the recorder call applyConstraints on the preview video track
(the track from previewHandoff.webcamStream) with the same constraints used in
the fresh-capture branch (e.g. width:1280, height:720, frameRate:30), await it
and catch errors so you can fall back to the original track if applyConstraints
fails; adjust references to webcamStream.current and the code that starts the
recorder so it uses the upgraded track.
---
Nitpick comments:
In `@electron/ipc/handlers.ts`:
- Around line 221-224: The get-session-type IPC handler returns
process.env.XDG_SESSION_TYPE directly which may be unexpected values; update the
ipcMain.handle("get-session-type", ...) implementation to normalize the session
type to only "wayland" or "x11": read XDG_SESSION_TYPE (and fall back to
checking WAYLAND_DISPLAY as before), lowercase it, then return "wayland" if it
equals "wayland" and return "x11" for any other value. Ensure you update the
logic inside the ipcMain.handle callback so consumers always receive one of the
two known values.
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 145-172: prevSourceId is declared inside the useEffect so it
resets whenever the effect re-runs causing duplicate startPreview calls; move
that state to a ref (e.g., prevSourceIdRef = useRef<string | null>(null)) at
component scope and replace uses of prevSourceId in the effect with
prevSourceIdRef.current, updating prevSourceIdRef.current = source.id after
calling startPreview(source.id) and when selecting a source so the previous
source id persists across effect runs; keep the rest of the checkSelectedSource
logic and the interval handling the same.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cb9512f1-35fc-4232-8232-fa7c1dd78fd9
📒 Files selected for processing (10)
electron/electron-env.d.tselectron/ipc/handlers.tselectron/main.tselectron/preload.tselectron/windows.tssrc/components/launch/LaunchWindow.tsxsrc/components/launch/LivePreview.tsxsrc/hooks/usePreviewStream.tssrc/hooks/useScreenRecorder.tssrc/vite-env.d.ts
Enable WebRTCPipeWireCapturer and ozone-platform-hint flags on Linux to support screen capture via PipeWire on Wayland sessions.
Expand from 500x155 fixed pill bar to 480x420 resizable window (380-640 width range) to accommodate the live preview area. Position bottom-right instead of bottom-center.
Add get-session-type IPC handler to detect display server type on Linux, enabling Wayland-aware source selection in the renderer.
Manages the MediaStream lifecycle for screen capture preview: - Starts/stops preview streams with source switching support - Handles webcam stream alongside screen capture - Supports stream detachment for seamless handoff to MediaRecorder (avoids double getUserMedia calls and Wayland re-prompts)
Real-time canvas-based preview that composites screen capture with a circular webcam PiP overlay. Renders at 30fps with throttling, caps internal resolution at 960px for GPU efficiency. Shows a placeholder when no source is selected.
Accept optional PreviewStreamHandoff in toggleRecording/startRecording to reuse existing preview MediaStreams instead of creating new ones. This avoids double getUserMedia calls and PipeWire re-prompts on Wayland. When a handoff is provided, video constraints are upgraded in-place.
Replace the 500x155 HUD pill bar with a full preview window featuring: - Live screen capture preview that starts when a source is selected - Canvas-composited webcam PiP overlay in the preview - Recording indicator in the title bar - Stream handoff from preview to recorder (no double getUserMedia) - Auto-restart preview after recording stops - Glass-morphism container styling
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/launch/LaunchWindow.tsx (1)
187-214:⚠️ Potential issue | 🟠 MajorPersist
prevSourceIdacross effect reruns.Line 189 recreates
prevSourceIdevery timerecordingchanges, so the same source looks “new” again after each stop. In the current flow that can callstartPreview(source.id)once from the polling effect and again from the 500 ms restart path, which risks duplicate capture restarts and extra portal prompts.💡 Proposed fix
-import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; ... + const prevSourceIdRef = useRef<string | null>(null); ... useEffect(() => { - let prevSourceId: string | null = null; - const checkSelectedSource = async () => { if (window.electronAPI) { const source = await window.electronAPI.getSelectedSource(); if (source) { setSelectedSource(source.name); setHasSelectedSource(true); - if (source.id !== prevSourceId && !recording) { - prevSourceId = source.id; + if (!recording && source.id !== prevSourceIdRef.current) { + prevSourceIdRef.current = source.id; startPreview(source.id); } } else { setSelectedSource("Screen"); setHasSelectedSource(false); + prevSourceIdRef.current = null; } } };Also applies to: 222-231
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/launch/LaunchWindow.tsx` around lines 187 - 214, The bug is that prevSourceId is reinitialized inside the useEffect on every rerun (dependent on recording/startPreview), causing the same source to be treated as new; move prevSourceId out of the effect and persist it across renders (e.g., use a React ref or component-level state) so checkSelectedSource can compare against the previous value consistently; update references to prevSourceId in the checkSelectedSource function and ensure cleanup/interval logic remains the same so startPreview(source.id) is only called when the actual source id changes.
♻️ Duplicate comments (1)
src/hooks/useScreenRecorder.ts (1)
456-459:⚠️ Potential issue | 🟠 MajorUpgrade the handed-off webcam track before starting
webcamRecorder.Lines 456-459 still reuse the preview webcam stream as-is, so handoff recordings can fall back to preview quality even though the fresh-capture branch requests 1280×720 at 30 fps. Apply the same best-effort
applyConstraints(...)upgrade on the reused track before creatingwebcamRecorder.💡 Proposed fix
if (previewHandoff?.webcamStream) { // Reuse preview webcam stream webcamStream.current = previewHandoff.webcamStream; + const webcamTrack = webcamStream.current.getVideoTracks()[0]; + if (webcamTrack) { + try { + await webcamTrack.applyConstraints({ + width: { ideal: WEBCAM_TARGET_WIDTH }, + height: { ideal: WEBCAM_TARGET_HEIGHT }, + frameRate: { + ideal: WEBCAM_TARGET_FRAME_RATE, + max: WEBCAM_TARGET_FRAME_RATE, + }, + }); + } catch { + // Best-effort upgrade; keep preview settings if unsupported. + } + } } else if (webcamEnabled) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useScreenRecorder.ts` around lines 456 - 459, When reusing a handed-off preview stream (previewHandoff.webcamStream) we must attempt to upgrade its track constraints to match the fresh-capture settings before creating the webcamRecorder; modify the branch that currently sets webcamStream.current = previewHandoff.webcamStream so that you get the MediaStreamTrack from previewHandoff.webcamStream (e.g., getVideoTracks()[0]) and call track.applyConstraints({ width: 1280, height: 720, frameRate: 30 }) in a best-effort try/catch, falling back silently on failure, then set webcamStream.current to the (possibly-upgraded) stream and proceed to create webcamRecorder as in the fresh-capture path; reference previewHandoff, webcamStream, webcamEnabled, webcamRecorder and applyConstraints when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 360-374: The select currently uses undefined names
(selectedDeviceId, setSelectedDeviceId, devices); update it to use the names
returned by useMicrophoneDevices: replace selectedDeviceId with selectedMicId,
setSelectedDeviceId with setSelectedMicId, and devices with micDevices, while
keeping the existing microphoneDeviceId and setMicrophoneDeviceId logic; ensure
the select value uses (microphoneDeviceId || selectedMicId), onchange calls
setSelectedMicId and setMicrophoneDeviceId, and the option list maps micDevices
(keying by device.deviceId and showing device.label) so the component
(LaunchWindow and the useMicrophoneDevices hook usage) compiles.
- Around line 384-492: The JSX control-bar has unbalanced tags causing a parse
error: remove the stray closing </button> after the restartRecording Tooltip and
the extraneous `)}` after the openVideoFile Tooltip, then rewrap the conditional
blocks so the outer divs and fragments are balanced; specifically, ensure the
block that renders the Record/Stop button, the conditional {recording &&
<Tooltip>...restartRecording...</Tooltip>} and the subsequent Tooltip buttons
(openVideoFile -> openProjectFile) are siblings inside the same container div,
and that you keep Tooltip components (and their inner buttons using
hudIconBtnClasses, openVideoFile, openProjectFile, restartRecording) properly
opened and closed with matching JSX tags and no leftover fragments or
parentheses.
---
Outside diff comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 187-214: The bug is that prevSourceId is reinitialized inside the
useEffect on every rerun (dependent on recording/startPreview), causing the same
source to be treated as new; move prevSourceId out of the effect and persist it
across renders (e.g., use a React ref or component-level state) so
checkSelectedSource can compare against the previous value consistently; update
references to prevSourceId in the checkSelectedSource function and ensure
cleanup/interval logic remains the same so startPreview(source.id) is only
called when the actual source id changes.
---
Duplicate comments:
In `@src/hooks/useScreenRecorder.ts`:
- Around line 456-459: When reusing a handed-off preview stream
(previewHandoff.webcamStream) we must attempt to upgrade its track constraints
to match the fresh-capture settings before creating the webcamRecorder; modify
the branch that currently sets webcamStream.current =
previewHandoff.webcamStream so that you get the MediaStreamTrack from
previewHandoff.webcamStream (e.g., getVideoTracks()[0]) and call
track.applyConstraints({ width: 1280, height: 720, frameRate: 30 }) in a
best-effort try/catch, falling back silently on failure, then set
webcamStream.current to the (possibly-upgraded) stream and proceed to create
webcamRecorder as in the fresh-capture path; reference previewHandoff,
webcamStream, webcamEnabled, webcamRecorder and applyConstraints when making
this change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b19ee7d9-96d0-415c-b086-b45a59d325af
📒 Files selected for processing (2)
src/components/launch/LaunchWindow.tsxsrc/hooks/useScreenRecorder.ts
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/launch/LaunchWindow.tsx (1)
246-253:⚠️ Potential issue | 🟡 Minormissing null check on
window.electronAPI— inconsistent with other calls.you're using optional chaining in
sendHudOverlayHide(line 262) andsendHudOverlayClose(line 269), butopenVideoFilejust yolo callswindow.electronAPI.openVideoFilePicker(). if electronAPI is undefined this will throw.nit: quick fix
const openVideoFile = async () => { + if (!window.electronAPI) return; const result = await window.electronAPI.openVideoFilePicker();or use optional chaining with early return if undefined.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/launch/LaunchWindow.tsx` around lines 246 - 253, The openVideoFile function calls window.electronAPI.openVideoFilePicker() without guarding for undefined; add the same null-check pattern used by sendHudOverlayHide/sendHudOverlayClose: first verify window.electronAPI exists (or use optional chaining) and early-return if missing, then call openVideoFilePicker(), and proceed to call setCurrentVideoPath and switchToEditor only when electronAPI and returned values are present (e.g., check result.success and result.path as already done).src/hooks/useScreenRecorder.ts (1)
47-62:⚠️ Potential issue | 🔴 Critical
togglePausedis returned but never defined — this crashes.kinda cursed: you're returning
togglePausedat line 724 but it literally doesn't exist anywhere in this file. you havesetPausedfrom useState at line 94 but no toggle function. this is either a compile error or a runtime ReferenceError waiting to happen.also the
UseScreenRecorderReturntype (lines 47-62) is missingpaused,elapsedSeconds, andtogglePausedthat you're actually returning — type doesn't match implementation.two fixes needed:
- add the missing
togglePausedfunction, or- if pause isn't needed, remove it from the return and update the type
LaunchWindow already destructures
togglePausedat line 83, so something needs to be returned here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/useScreenRecorder.ts` around lines 47 - 62, The hook currently returns togglePaused but never defines it and the UseScreenRecorderReturn type is missing paused, elapsedSeconds, and togglePaused; add a togglePaused function that flips the existing setPaused state (from useState) and returns the new paused value (e.g., const togglePaused = () => { setPaused(p => { const next = !p; return next }); return /* new value */ }), include paused and elapsedSeconds in the UseScreenRecorderReturn type, and ensure the hook's returned object includes the newly defined togglePaused, paused, and elapsedSeconds so LaunchWindow (which destructures togglePaused) keeps working.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 359-362: The "get-session-type" IPC handler currently returns raw
XDG_SESSION_TYPE which can be unpredictable; update the
ipcMain.handle("get-session-type") implementation to canonicalize and constrain
output to exactly "wayland" or "x11": on non-Linux keep returning "x11", on
Linux read process.env.XDG_SESSION_TYPE and process.env.WAYLAND_DISPLAY,
normalize the env value by trimming and lower-casing, and return "wayland" only
if the normalized XDG_SESSION_TYPE === "wayland" or WAYLAND_DISPLAY is present;
otherwise always return "x11".
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 107-108: The component never updates the local elapsed state and
keeps unused recordingStart state: useScreenRecorder()'s elapsedSeconds
(destructured earlier) should be used or synced to fix the REC timer; replace
usages of the local elapsed state with elapsedSeconds from useScreenRecorder OR
add an effect that calls setElapsed(elapsedSeconds) when elapsedSeconds changes,
and remove the dead recordingStart/setRecordingStart state if it's not needed
(references: useScreenRecorder, elapsedSeconds, elapsed, setElapsed,
recordingStart, setRecordingStart, and the REC timer render locations).
---
Outside diff comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 246-253: The openVideoFile function calls
window.electronAPI.openVideoFilePicker() without guarding for undefined; add the
same null-check pattern used by sendHudOverlayHide/sendHudOverlayClose: first
verify window.electronAPI exists (or use optional chaining) and early-return if
missing, then call openVideoFilePicker(), and proceed to call
setCurrentVideoPath and switchToEditor only when electronAPI and returned values
are present (e.g., check result.success and result.path as already done).
In `@src/hooks/useScreenRecorder.ts`:
- Around line 47-62: The hook currently returns togglePaused but never defines
it and the UseScreenRecorderReturn type is missing paused, elapsedSeconds, and
togglePaused; add a togglePaused function that flips the existing setPaused
state (from useState) and returns the new paused value (e.g., const togglePaused
= () => { setPaused(p => { const next = !p; return next }); return /* new value
*/ }), include paused and elapsedSeconds in the UseScreenRecorderReturn type,
and ensure the hook's returned object includes the newly defined togglePaused,
paused, and elapsedSeconds so LaunchWindow (which destructures togglePaused)
keeps working.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: fbba8748-9780-40d5-8cc7-3fbdc2f15242
📒 Files selected for processing (3)
electron/ipc/handlers.tssrc/components/launch/LaunchWindow.tsxsrc/hooks/useScreenRecorder.ts
|
Hey @hamidlabs, thanks for the PR! I ran tsc --noEmit and found a few issues that need to be fixed before this can be merged:
Also had two JSX structural bugs (mismatched tags) that prevented the app from compiling at all. Could you take a look and update accordingly? Happy to help if anything is unclear. |
Summary
Adds a real-time live preview to the launch window, similar to OBS Studio's preview panel. Users can now see exactly what will be recorded before and during recording.
requestAnimationFrameloop, capped at 960px internal resolution for GPU efficiencyMediaRecorderwhen recording starts — no doublegetUserMediacalls, no Wayland PipeWire re-promptsWebRTCPipeWireCapturerandozone-platform-hintflags, adds session type detection IPC for Wayland-aware behaviorChanges
electron/main.tselectron/windows.tselectron/ipc/handlers.tsget-session-typeIPC handlerelectron/preload.tsgetSessionTypeto rendererelectron/electron-env.d.tssrc/vite-env.d.tssrc/hooks/usePreviewStream.tssrc/hooks/useScreenRecorder.tsPreviewStreamHandoffto reuse preview streamssrc/components/launch/LivePreview.tsxsrc/components/launch/LaunchWindow.tsxTest plan
Summary by CodeRabbit
New Features
Refactor